﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Drawing;
using SharpShell.Attributes;
using SharpShell.SharpContextMenu;
using System.Runtime.InteropServices;
using System.IO;
using System.Diagnostics;
using System.Security.Cryptography;
using FileCompareByHash;
using System.Reflection;
using System.Net;
using System.Text.RegularExpressions;
using System.Globalization;
using Microsoft.Win32;
using System.Threading;
namespace FileCompareByHashExtension
{
    [ComVisible(true)]
    [COMServerAssociation(AssociationType.AllFiles)]
    public class MultiFileCompareByHash : SharpContextMenu
    {
        /// <summary>
        /// Determines whether this instance can a shell context show menu, given the specified selected file list.
        /// </summary>
        /// <returns>
        ///   <c>true</c> if this instance should show a shell context menu for the specified file list; otherwise, <c>false</c>.
        /// </returns>
        protected override bool CanShowMenu()
        {
            //  MessageBox.Show("menu == null is " + (_menu == null).ToString());
            //  We can show the item only for a single selection.
            int itemcnt = SelectedItemPaths.Count();
            if (itemcnt == 0)
                return false;
            return true;
        }
        ContextMenuStrip _menu;
        ToolStripMenuItem _binaryCompareItem;
        ToolStripMenuItem _textCompareItem;
        ToolStripMenuItem _binaryCompare2Items;
        ToolStripMenuItem _textCompare2Items;
        ToolStripMenuItem _md5Item;
        ToolStripMenuItem _sha1Item;
        ToolStripMenuItem _sha256Item;
        ToolStripMenuItem _aboutItem;
        ToolStripMenuItem _rootItem;
        /// <summary>
        /// Creates the context menu. This can be a single menu item or a tree of them.
        /// </summary>
        /// <returns>
        /// The context menu for the shell context menu.
        /// </returns>
        protected override ContextMenuStrip CreateMenu()
        {

            //  Create the menu strip.
            _menu = new ContextMenuStrip();

            //  Create a menu item to hold all of the subitems.
            _rootItem = new ToolStripMenuItem
            {
                Text = "Compare&&Hash",
                Image = FileCompareByHash.Resource.compare22

            };

            //  Now add the child items.
            _binaryCompareItem = new ToolStripMenuItem{Text = "Binary Compare with...",};
            _binaryCompareItem.Click += (sender, args) => CompareAgainst(sender as ToolStripMenuItem);

            _textCompareItem = new ToolStripMenuItem { Text = "Text Compare with..." };
            _textCompareItem.Click += (sender, args) => CompareAgainst(sender as ToolStripMenuItem);


            _binaryCompare2Items = new ToolStripMenuItem { Text = "Compare binary files...", };
            _binaryCompare2Items.Click += (sender, args) => ShowComparison(true);

            _textCompare2Items = new ToolStripMenuItem { Text = "Compare text files...", };
            _textCompare2Items.Click += (sender, args) => ShowComparison(false);

            _md5Item = new ToolStripMenuItem{Text = "Calculate MD5",};
            _md5Item.Click += (sender, args) => ShowHash<MD5CryptoServiceProvider>();

            _sha1Item = new ToolStripMenuItem{Text = "Calculate SHA1",};
            _sha1Item.Click += (sender, args) => ShowHash<SHA1CryptoServiceProvider>();

            _sha256Item = new ToolStripMenuItem{Text = "Calculate SHA256",};
            _sha256Item.Click += (sender, args) => ShowHash<SHA256CryptoServiceProvider>();

            _aboutItem = new ToolStripMenuItem{Text = "About...",};
            _aboutItem.Click += (sender, args) => About(this);

            //  Add all the items.
            _rootItem.DropDownItems.Add(_binaryCompareItem);
            _rootItem.DropDownItems.Add(_textCompareItem);
            _rootItem.DropDownItems.Add(_binaryCompare2Items);
            _rootItem.DropDownItems.Add(_textCompare2Items);
            _rootItem.DropDownItems.Add(new ToolStripSeparator());
            _rootItem.DropDownItems.Add(_md5Item);
            _rootItem.DropDownItems.Add(_sha1Item);
            _rootItem.DropDownItems.Add(_sha256Item);
            _rootItem.DropDownItems.Add(new ToolStripSeparator());
            _rootItem.DropDownItems.Add(_aboutItem);
            //  Add the item to the context menu.
            _menu.Items.Add(_rootItem);

            if (_menu != null)
            {
                UpdateMenu();
            }
            //  Return the menu.
            return _menu;
        }
        private void UpdateMenu()
        {
            int itemcnt = SelectedItemPaths.Count();
            switch (itemcnt)
            {
                case 1:
                    _rootItem.DropDownItems.Remove(_textCompare2Items);
                    _rootItem.DropDownItems.Remove(_binaryCompare2Items);
                    break;
                case 2:
                    _rootItem.DropDownItems.Remove(_textCompareItem);
                    _rootItem.DropDownItems.Remove(_binaryCompareItem);
                    break;
                default:
                    _rootItem.DropDownItems.Remove(_textCompare2Items);
                    _rootItem.DropDownItems.Remove(_binaryCompare2Items);
                    _rootItem.DropDownItems.Remove(_textCompareItem);
                    _rootItem.DropDownItems.Remove(_binaryCompareItem);
                    _rootItem.DropDownItems.RemoveAt(0);//the separator
                    break;
            }
        }
        public static void About(MultiFileCompareByHash scm)
        {
            NativeWindow nw = null;
            if (scm != null && scm.CurrentInvokeCommandInfo.WindowHandle != IntPtr.Zero)
            {
                nw = new NativeWindow();
                nw.AssignHandle(scm.CurrentInvokeCommandInfo.WindowHandle);
            }
            AboutForm af = new AboutForm();
            if (nw == null)
                af.ShowDialog();
            else
                af.ShowDialog(nw);
        }

        void CompareAgainst(ToolStripMenuItem item)
        {

            string file1 = SelectedItemPaths.FirstOrDefault();
            if (file1 == null || !File.Exists(file1))
                return;
            string ext = Path.GetExtension(file1);
            OpenFileDialog openFileDialog = new OpenFileDialog();

            openFileDialog.InitialDirectory = Path.GetDirectoryName(file1);
            openFileDialog.Filter = string.Format("specific files (*{0})|*{0}|All files (*.*)|*.*", ext);
            openFileDialog.FilterIndex = 1;
            openFileDialog.RestoreDirectory = true;
            openFileDialog.Title = "Comparing against " + file1;
            NativeWindow nw = null;
            if (CurrentInvokeCommandInfo.WindowHandle != IntPtr.Zero)
            {
                nw = new NativeWindow();
                nw.AssignHandle(CurrentInvokeCommandInfo.WindowHandle);
            }
            try
            {
                if (nw == null)
                {
                    // MessageBox.Show("Compare against file, Hwnd = " + CurrentInvokeCommandInfo.WindowHandle.ToString("X"));
                    if (openFileDialog.ShowDialog() != DialogResult.OK)
                        return;

                }
                else
                {
                    if (openFileDialog.ShowDialog(nw) != DialogResult.OK)
                        return;
                }

                string file2 = openFileDialog.FileName;
                // if (item == _binaryCompareItem)
                ShowComparison(item == _binaryCompareItem, file1, file2, nw);


            }
            catch (Exception ex)
            {

                MessageBox.Show("Error: Could not read file from disk. Original error: " + ex.Message);
            }
            finally
            {

                CheckOnline4NewVersion(nw);
                if (nw != null)
                    nw.ReleaseHandle();

            }

        }

        private void ShowComparison(bool bIsBinary, string file1 = null, string file2 = null, NativeWindow nw = null)
        {
            try
            {
                if (nw == null && CurrentInvokeCommandInfo.WindowHandle != IntPtr.Zero)
                {
                    nw = new NativeWindow();
                    nw.AssignHandle(CurrentInvokeCommandInfo.WindowHandle);
                }
                if (file1 == null)
                {
                    string[] files = SelectedItemPaths.ToArray();
                    if (files.Length != 2)
                    {
                        if (nw == null)
                            MessageBox.Show("2 files should be selected", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                        else
                            MessageBox.Show(nw, "2 files should be selected", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                        return;
                    }
                    file1 = files[0];
                    file2 = files[1];
                }

                FileInfo fi1 = new FileInfo(file1);
                FileInfo fi2 = new FileInfo(file2);

                bool bSame = FilesContentsAreEqual(fi1, fi2);
                string msg = string.Format(bSame ? "The files '{0}' and '{1}' are identical.{2}Do you want to see more details?" : "The files '{0}' and '{1}' are different.{2}Do you want to see more details?",
                    Path.GetFileName(file1), Path.GetFileName(file2), Environment.NewLine);

                DialogResult res = DialogResult.No;
                if (bIsBinary)
                {

                    if (nw == null)
                        res = MessageBox.Show(msg, "Binary comparison results", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
                    else
                        res = MessageBox.Show(nw, msg, "Binary comparison results", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
                }
                else
                    res = DialogResult.Yes;
                if (res == DialogResult.Yes)
                {

                    using (Form frm = bIsBinary ? (Form)new DetailedComparison(fi1, fi2, bSame) : (Form)new TextDiffForm(fi1, fi2))
                    {
                        if (nw == null)
                            frm.ShowDialog();
                        else
                            frm.ShowDialog(nw);
                    }

                }
            }
            catch (Exception ex)
            {

                MessageBox.Show(ex.Message, "Error");
            }
            finally
            {

                CheckOnline4NewVersion(nw);
                if (nw != null)
                    nw.ReleaseHandle();

            }
        }
        const int _period = 15;
        public static void CheckOnline4NewVersion(NativeWindow nw)
        {
            try
            {
                DateTime lastCheck = MultiFileCompareByHash.LastOnlineCheck;
                if (lastCheck == DateTime.Today)
                    lastCheck = LastOnlineCheck = DateTime.Now;
                bool bIsnew = MultiFileCompareByHash.IsNewerThanCurrent(float.Parse(MultiFileCompareByHash.LastOnlineVer, System.Globalization.CultureInfo.InvariantCulture.NumberFormat));
                bool bPending = lastCheck + new TimeSpan(_period, 0, 0, 0) < DateTime.Today;//shows only once a day, not first day

                if (bPending && bIsnew)
                {
                    DialogResult res = DialogResult.No;
                    string msg = string.Format("A newer version is available online.{0}Do you want to download it?", Environment.NewLine);
                    if (nw == null)
                        res = MessageBox.Show(msg, "Do you want to update?", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
                    else
                        res = MessageBox.Show(nw, msg, "Do you want to update?", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
                    if (res == DialogResult.Yes)
                    {
                        string downloadUrl;
                        float onlineVersion;
                        //keep the time component to signal that the user haven't seen the upgrade Msg
                        MultiFileCompareByHash.LastOnlineCheck = DateTime.Now;
                        if (MultiFileCompareByHash.NewOnlineVersionExists(out downloadUrl, out onlineVersion))
                        {

                            System.Diagnostics.Process.Start(string.IsNullOrEmpty(downloadUrl) ? BaseURL : downloadUrl);
                        }
                        return;
                    }
                }

                if (bPending)
                {
                    Thread tr = new Thread(new ThreadStart(() =>
                    {
                        string downloadUrl;
                        float onlineVersion;
                        //keep the time component to signal that the user haven't seen the upgrade Msg
                        MultiFileCompareByHash.LastOnlineCheck = DateTime.Now;
                        if (MultiFileCompareByHash.NewOnlineVersionExists(out downloadUrl, out onlineVersion))
                        {
                            MultiFileCompareByHash.LastOnlineVer = onlineVersion.ToString(CultureInfo.InvariantCulture.NumberFormat);
                        }
                    }));
                    tr.IsBackground = false;
                    tr.Start();
                }
            }
            catch (Exception)
            {

                // throw;
            }
        }
       
        void ShowHash<T>() where T : HashAlgorithm, new()
        {
            //  Builder for the output string.
            var builder = new StringBuilder(200);

            //  Go through each file.
            string hash = null;
            foreach (var filePath in SelectedItemPaths)
            {
                //HashAlgorithm->MD5->MD5CryptoServiceProvider
                hash = CalcHash<T>(filePath);
                builder.AppendLine(string.Format("{0} for '{1}'", hash, Path.GetFileName(filePath)));
            }

            //  Show the ouput.
            NativeWindow nw = null;
            if (CurrentInvokeCommandInfo.WindowHandle != IntPtr.Zero)
            {
                nw = new NativeWindow();
                nw.AssignHandle(CurrentInvokeCommandInfo.WindowHandle);
                //MessageBox.Show(builder.ToString(), typeof(T).BaseType.Name + " file(s) hash");
            }

            try
            {
                if (SelectedItemPaths.Count() == 1)
                {
                    DialogResult res = DialogResult.No;
                    //builder.AppendFormat("{0}Do you want to copy it to the Clipboard?", Environment.NewLine);
                    builder.Append("Do you want to copy the hash to the Clipboard?");
                    if (nw == null)
                        res = MessageBox.Show(builder.ToString(), typeof(T).BaseType.Name + " file hash", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
                    else
                        res = MessageBox.Show(nw, builder.ToString(), typeof(T).BaseType.Name + " file hash", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
                    if (res == DialogResult.Yes)
                        Clipboard.SetText(hash);
                }
                else
                {
                    if (nw == null)
                        MessageBox.Show(builder.ToString(), typeof(T).BaseType.Name + " files hash");
                    else
                        MessageBox.Show(nw, builder.ToString(), typeof(T).BaseType.Name + " files hash");
                }

            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "Exception");
            }
            finally
            {
                CheckOnline4NewVersion(nw);
                if (nw != null)
                    nw.ReleaseHandle();
                builder.Clear();
            }



        }

        public static string CalcHash<T>(string filename) where T : HashAlgorithm, new()
        {
            //HashAlgorithm->MD5->MD5CryptoServiceProvider
            using (HashAlgorithm csp = new T())//same as MD5.Create()
            {
                try
                {
                    using (var stream = File.OpenRead(filename))
                    {
                        string strHash = BitConverter.ToString(csp.ComputeHash(stream)).Replace("-", "").ToLower();
                        return strHash;
                        // return string.Format("{0} is {1} hash for '{2}'", strHash, csp.GetType().BaseType.Name, Path.GetFileName(filename));
                    }
                }
                catch (Exception ex)
                {

                    return string.Format("Can't calculate {0} for {1} because {2}", csp.GetType().BaseType.Name, filename, ex.Message);
                }
            }
        }

        public static bool FilesContentsAreEqual(FileInfo fileInfo1, FileInfo fileInfo2)
        {
            bool result;

            if (fileInfo1.Length != fileInfo2.Length)
            {
                result = false;
            }
            else
            {
                using (var file1 = fileInfo1.OpenRead())
                {
                    using (var file2 = fileInfo2.OpenRead())
                    {
                        result = StreamsContentsAreEqual(file1, file2);
                    }
                }
            }

            return result;
        }

        private static bool StreamsContentsAreEqual(Stream stream1, Stream stream2)
        {
            const int bufferSize = 2048 * 2;
            var buffer1 = new byte[bufferSize];
            var buffer2 = new byte[bufferSize];

            while (true)
            {
                int count1 = stream1.Read(buffer1, 0, bufferSize);
                int count2 = stream2.Read(buffer2, 0, bufferSize);

                if (count1 != count2)
                {
                    return false;
                }

                if (count1 == 0)
                {
                    return true;
                }

                int iterations = (int)Math.Ceiling((double)count1 / sizeof(Int64));
                for (int i = 0; i < iterations; i++)
                {
                    if (BitConverter.ToInt64(buffer1, i * sizeof(Int64)) != BitConverter.ToInt64(buffer2, i * sizeof(Int64)))
                    {
                        return false;
                    }
                }
            }
        }

        public const string BaseURL = "http://filecompareandhash.codeplex.com/";
        public const string AppHive = @"Software\FileCompareAndHash";

        internal static string FindFirstMatchingGroup(string input, string regex, string group, RegexOptions opt)
        {
            string result = null;
            try
            {
                Match m = Regex.Match(input, regex, opt);
                if (m.Success && m.Groups[group].Success)
                {
                    result = m.Groups[group].Value;
                }
            }
            catch
            {

                result = null;
            }
            return result;
        }
        static public string LastOnlineVer
        {
            get
            {
                RegistryKey reg = null;
                try
                {
                    reg = Registry.CurrentUser.OpenSubKey(AppHive, false);
                    if (reg != null)
                    {
                        return reg.GetValue("LastOnlineVersion", "0.0") as string;
                    }
                    else
                    {
                        return "9.9";
                    }
                }
                catch (Exception)
                {

                    return "9.9";
                }
                finally
                {
                    if (reg != null)
                        reg.Close();
                }
            }
            set
            {
                using (RegistryKey reg = Registry.CurrentUser.CreateSubKey(AppHive, RegistryKeyPermissionCheck.ReadWriteSubTree))
                {
                    reg.SetValue("LastOnlineVersion", value, RegistryValueKind.String);
                }
            }
        }
        static public DateTime LastOnlineCheck
        {
            get
            {
                RegistryKey reg = null;
                try
                {
                    reg = Registry.CurrentUser.OpenSubKey(AppHive, false);
                    if (reg != null)
                    {

                        long ticks = (long)reg.GetValue("LastOnlineCheck", 0L);
                        return (ticks == 0L) ? DateTime.Today : new DateTime(ticks);
                    }
                    else
                    {
                        return DateTime.Today;
                    }
                }
                catch (Exception)
                {

                    return DateTime.Today;
                }
                finally
                {
                    if (reg != null)
                        reg.Close();
                }
            }
            set
            {
                using (RegistryKey reg = Registry.CurrentUser.CreateSubKey(AppHive, RegistryKeyPermissionCheck.ReadWriteSubTree))
                {
                    reg.SetValue("LastOnlineCheck", (ulong)value.Ticks, RegistryValueKind.QWord);
                }
            }
        }
        public static bool NewOnlineVersionExists(out string downloadUrl, out float onlineVersion)
        {

            bool isnew = false;
            downloadUrl = null;
            onlineVersion = 0;

            downloadUrl = BaseURL;
            try
            {

                using (WebClient client = new WebClient())
                {
                    string result = client.DownloadString(downloadUrl);
                    string version = FindFirstMatchingGroup(result, @"<td>\s*File[ ]*compare[ ]*and[ ]*hash[ ]*shell[ ]*extension[ ]*(?<Version>[0-9,.]+)\s*</td>", "Version", RegexOptions.IgnoreCase).Trim();
                    // Logging.Instance.Log(3, "Online version:" + version);
                    if (!string.IsNullOrEmpty(version))
                    {

                        LastOnlineVer = version;
                        if (!float.TryParse(version, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture.NumberFormat, out onlineVersion))
                        {
                            //Logging.Instance.Log(3, "Can't parse version:" + version);
                            return false;
                        }
                        //onlineVersion = float.Parse(version, System.Globalization.CultureInfo.InvariantCulture.NumberFormat);
                        isnew = IsNewerThanCurrent(onlineVersion);
                        if (isnew)
                        {
                            // Logging.Instance.Log(3, version + " IsNewerThanCurrent");
                            string latestUrl = FindFirstMatchingGroup(result, "href=\\\"(?<URL>" + BaseURL + "releases/view/[0-9]+)\\\"", "URL", RegexOptions.Multiline);
                            if (string.IsNullOrEmpty(latestUrl))
                            {
                                //  Logging.Instance.Log(1, "Can't find first download url");
                                return true;
                            }
                            else
                                downloadUrl = latestUrl;
                            // Logging.Instance.Log(1, "first download url " + downloadUrl);
                            result = client.DownloadString(downloadUrl);
                            //get download link for all setups
                            latestUrl = FindFirstMatchingGroup(result, "href=\\\"(?<URL>" + BaseURL + "downloads/get/[0-9]+)\\\"" + ".+>FCandHashInstaller_" + version + ".zip\\s*</a>", "URL", RegexOptions.Multiline | RegexOptions.IgnoreCase);
                            if (string.IsNullOrEmpty(latestUrl))//no urll for download all, get the first download link on the page
                                latestUrl = FindFirstMatchingGroup(result, "href=\\\"(?<URL>" + BaseURL + "downloads/get/[0-9]+)\\\"", "URL", RegexOptions.Multiline | RegexOptions.IgnoreCase);
                            if (string.IsNullOrEmpty(latestUrl) || latestUrl.Length <= 20)//can't pass the smell test
                            {
                                return true;//keep the last valid url
                            }
                            downloadUrl = latestUrl;
                            //Logging.Instance.Log(1, "Best download url for all:" + downloadUrl);
                        }
                    }

                }
            }
            catch (Exception ex)
            {
                //  Logging.Instance.Log(1, "Exception in IsnewVersion:" + ex.Message);
            }
            return isnew;
        }

        public static bool IsNewerThanCurrent(float onlineVersion)
        {
            Assembly crt = Assembly.GetExecutingAssembly();

            Version v = crt.GetName().Version;
            float crtVersion = float.Parse(string.Format("{0}.{1}", v.Major, v.Minor), System.Globalization.CultureInfo.InvariantCulture.NumberFormat);
            return crtVersion < onlineVersion;
        }
    }

}

namespace FileCompareByHash
{
static class Levensthein
{
    //*****************************
    // Compute Levenshtein distance
    //*****************************
    internal static void Reset()
    {
        _costMatrix = null;
    }
    static ushort[,] _costMatrix = null;
    static internal ushort LD(string s, string t)
    {
        ushort n = (ushort)s.Length; //length of s
        ushort m = (ushort)t.Length; //length of t
        // Step 1
        if (n == 0) return m;
        if (m == 0) return n;
        //reallocate only if necessary
        if (_costMatrix == null || _costMatrix.GetUpperBound(0) < n || _costMatrix.GetUpperBound(1) < m)
            _costMatrix = new ushort[n + 1, m + 1]; // matrix
        // Step 2
        for (ushort i = 0; i <= n; _costMatrix[i, 0] = i++) ;
        for (ushort j = 0; j <= m; _costMatrix[0, j] = j++) ;
        // Step 3
        ushort cost; // cost
        for (ushort i = 1; i <= n; i++)
        {
            //Step 4
            for (ushort j = 1; j <= m; j++)
            {
                // Step 5
                if (s[i - 1] == t[j - 1])
                    cost = 0;
                else
                    cost = 1;
                // Step 6
                _costMatrix[i, j] = (ushort)System.Math.Min(System.Math.Min(_costMatrix[i - 1, j] + 1, _costMatrix[i, j - 1] + 1),
                            _costMatrix[i - 1, j - 1] + cost);
            }
        }
        // Step 7
        return _costMatrix[n, m];
    }
    internal static double Differentiation(string txt1, string txt2, bool bCaseSensitive = true, Func<double, double>f = null)
    {
        double lDist = bCaseSensitive ? Levensthein.LD(txt1, txt2) : Levensthein.LD(txt1.ToLower(), txt2.ToLower());
        if (lDist > 0)
        {
            lDist /= Math.Max(txt1.Length, txt2.Length);//range is 0.0 to 1.0
            if (f != null)
                lDist = f(lDist);
            return lDist;

        }
        else
            return 0.0;
    }
}
}